<?php
/*******************************************************************************
Rd_Ils_Sirsi
Implementation of NCSU's Ils Sirsi

Created by Karl Doerr and Troy Hurteau, NCSU Libraries, NC State University (libraries.opensource@ncsu.edu).

This file is part of NCSU's distribution of ReservesDirect. This version has not been downloaded from Emory University
or the original developers of ReservesDirect. Neither Emory University nor the original developers of ReservesDirect have authorized
or otherwise endorsed or approved this distribution of the software.

Copyright (c) 2004-2006 Emory University, Atlanta, Georgia.

Licensed under the NCSU ReservesDirect License, Version 2.0 (the "License"); 
you may not use this file except in compliance with the License. You may obtain a copy of the full License at
 http://www.lib.ncsu.edu/it/opensource/

ReservesDirect is distributed in the hope that it will be useful,
but is distributed "AS IS" and WITHOUT ANY WARRANTY, without even the
implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
PURPOSE, and without any warranty as to non-infringement of any third
party's rights. See the License for the specific language governing permissions and limitations under the License.

The original version of ReservesDirect is located at:
http://www.reservesdirect.org/

This version of ReservesDirect, distributed by NCSU, is located at:
http://code.google.com/p/reservesdirect-ncsu/

*******************************************************************************/
require_once(APPLICATION_PATH . '/lib/Rd/Ils/Abstract.php');
require_once(APPLICATION_PATH . '/lib/Rd/Ils/class_xml_check.php');
require_once(APPLICATION_PATH . '/lib/Rd/Ils/SirsiResult.php');

class Rd_Ils_Sirsi extends Rd_Ils_Abstract
{
	protected $create_reserve_script	= '';
	protected $holdings_script			= '';

	# if the following is set, it will be used in preference
	# to the sel_item_server congifuration under z* properties

	protected $_barcodeUrl = '';

	protected $zReflector = '';
	protected $zHost = '';
	protected $zPort = '';
	protected $zselItemServerPort = '';
	protected $zDB = '';
	
	protected $_shelvingKeyUrl = '';
	
	protected $_catalogLookupService = '';
	
	public  $_ilsName = 'Sirsi'; //#TODO fix this access level...

	public function __construct(){

		global $g_reserveScript, $g_holdingsScript, $g_zReflector, $g_zReflector, $g_zhost, $g_zport, $g_zcisport, $g_zdb;

		$this->create_reserve_script = $g_reserveScript;
		$this->_barcodeUrl = Rd_Registry::get('root:barcodeSearchServiceUrl');
		$this->holdings_script = $g_holdingsScript;
		$this->zReflector = $g_zReflector;
		$this->zHost = $g_zhost;
		$this->zPort = $g_zport;
		$this->zselItemServerPort = $g_zcisport;
		$this->zDB = $g_zdb;
		$this->_shelvingKeyUrl = Rd_Registry::get('root:shelvingKeyServiceUrl');
		$this->_catalogLookupService = Rd_Registry::get('root:titleIdSearchUrl');
	}

	protected function setILSName()
	{
		$this->_ilsName = 'Sirsi';
	}
	
	public function getName(){
		return $this->_ilsName;
	} 

	
	protected function setReservableFormats()
	{
		$this->_reservable_formats = array('MONOGRAPH', 'MULTIMEDIA', 'ELECTRONIC');
	}

	public function createReserve($request, $reserve)
	{
		$barcode  = $request['barcode'];
		$copy = $request['copyId'];
		$libraryId = $request['libraryId'];
		$circRule = $request['circRule'];
		$altCircRule = $request['altCircRule'];
		/*
		 try {
			$io = new InstructorObject();
			$borrower = $io->find($borrower_user_id);		
			$borrower->getInstructorAttributes();
			$borrowerID = $borrower->instructor_attributes->ils_user_id;
		} catch (Exception $e) {
			return new Rd_Sirsi_Result(Rd_Sirsi_Result::FAILURE_IDENTITY_NOT_FOUND, 'Borrower Not Found');
		}
		 */		
		$borrowerId = $request['instructorIlsId'];
		$term = $request['term'];	
		
		$expiration = $request['expiration'];//$reserve->expiration;
		$dateParts = explode('-', $expiration); //array($Y,$M,$D)
		$eDate = "{$dateParts[1]}/{$dateParts[2]}/{$dateParts[0]}";//"$M/$D/$Y";

		$reservesDesk = new library($libraryId);

		//$desk = $reservesDesk->getReserveDesk();
		$desk = $reservesDesk->getILS_prefix();
		$course = strtoupper($reservesDesk->getILS_prefix() . $term);		

		list($Y,$M,$D) = explode('-', $expiration);
		$eDate = "{$M}/{$D}/{$Y}";
	    
		$url = $this->create_reserve_script . "?itemID={$barcode}&borrowerID={$borrowerId}&courseID={$course}&reserve_desk={$desk}&circ_rule={$circRule}&alt_circ={$altCircRule}&expiration={$eDate}&cpy={$copy}";
		$message = "Holding URL: {$url}<br/>\n";
		$curl = curl_init($url);
		curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
		curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, true); //#TODO doesn't follow redirects to https
		$returnStatus = curl_exec($curl);
		$status = curl_getinfo($curl, CURLINFO_HTTP_CODE);
		if (200 != $status) {
			Rd_Debug::outData(array('failed_curl_sirsi_resControl' => curl_getinfo($curl)));
			return new Rd_Sirsi_Result(Rd_Sirsi_Result::FAILURE, 'ILS did not respond.');
		}
		curl_close($curl);
        
        $returnStatus = preg_replace('/<head>.*<\/head>/i', '', $returnStatus);
        $returnStatus = preg_replace('/<[A-z]*>/', '', $returnStatus);
        $returnStatus = preg_replace('/<\/[A-z]*>/', '', $returnStatus);

        $returnStatus = preg_replace('/<!.*\">/', '', $returnStatus);
        $returnStatus = preg_replace("/\n/", '', $returnStatus);

        if(!preg_match('/outcome=OK/i', $returnStatus))
        {
        	return new Rd_Sirsi_Result(Rd_Sirsi_Result::FAILURE_UNCATEGORIZED, "{$message}There was a problem setting the location and circ-rule in {$this->_ilsName}. <br/>{$this->_ilsName} returned:  {$returnStatus}.",NULL,FALSE);
        } else {
        	return new Rd_Sirsi_Result(Rd_Sirsi_Result::SUCCESS, "{$message}Location and circ-rule have been successfully set in {$this->_ilsName}", NULL, FALSE);
		}
	}

	
	public function displayReserveForm() //#TODO deprecate
	{
		return '';
	}

	public function getHoldings($key, $keyType = 'barcode')
	{
		
		if(empty($key) || empty($keyType)) {
			return array();
		}

		$rs = array();
		
		$key = preg_replace('/ocm/','o',$key);
		
		Rd_Debug::out("Holding Script at: {$this->holdings_script}?key={$key}&key_type={$keyType}<br />");
		
		$fp = fopen("{$this->holdings_script}?key={$key}&key_type={$keyType}", 'rb'); //#TODO Curl this bad boy
		if(!$fp) {
			throw new RD_Ils_Exception("Could not get holdings");
		}
		while (!feof ($fp)) {
			array_push($rs, @fgets($fp, 1024));
		}
		$returnStatus = join($rs, "");

		Rd_Debug::out("return status: " . $returnStatus);
		
		if(preg_match("/Outcome=OK\n/", $returnStatus))
		{
			list($devnull, $holdings) = explode("Outcome=OK\n", $returnStatus);

			$thisCopies = explode("\n", $holdings);

			$j = 0;
			for($i = 0; $i < (count($thisCopies) - 1); $i++)
			{
				if (strpos($thisCopies[$i], '|') !== false) {				
					list($devnull, $devnull, $copy, $callnum, $loc, $type, $bar, $library, $status, $reservesDesk) = explode('|', $thisCopies[$i]);
					if ('' != $copy && '' != $callnum)
					{
						$tmpArray[$j]['copy']		= trim($copy);
						$tmpArray[$j]['callNum']	= trim($callnum);
						$tmpArray[$j]['loc']		= trim($loc);
						$tmpArray[$j]['type']		= trim($type);
						$tmpArray[$j]['bar']		= trim($bar);
						$tmpArray[$j]['library']	= trim($library);
						$j++;
					}
				}
 			}

			return $tmpArray;
		} else return array();
	}
	
	public function getDetails($catkey) { //#TODO before 3.0 refactor this to accept the normal identifier/id params
        $rawKey = $this->filterIdentifier($catKey);
        $curl = curl_init();
        $options = array(
            CURLOPT_URL => $this->_catalogLookupService . $rawKey,
            CURLOPT_HEADER => false,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_CONNECTTIMEOUT => 1
        );
        curl_setopt_array($curl, $options);
        $result = curl_exec($curl);
		$status = curl_getinfo($curl, CURLINFO_HTTP_CODE);
        if ($status != 200) {
        	Rd_Debug::out(curl_error($curl));
        }
        curl_close($curl);
        return json_decode($result,true);
	}
	
	public function getSummary($catkey) { //#TODO before 3.0 refactor this to accept the normal identifier/id params
		if(strpos('NCLIVE', $catKey) === 0){
            return "NC Live Resources are not supported at this time. Unsupported Catalog Key: {$catKey}";
        }
		$resultObj = $this->getDetails($catkey);
        $item = (
        	array_key_exists('searchResponse', $resultObj)
        		&& array_key_exists('item', $resultObj['searchResponse'])
        	? $resultObj['searchResponse']['item']
        	: NULL
        );
        $holdings = (
        	$item
        		&& array_key_exists('holdings', $item)
        		&& array_key_exists('library', $item['holdings'])
        	? $item['holdings']['library']
        	: NULL
        );

        $resultString = '';
        $resultString .= ($item && array_key_exists('author', $item)) ? "{$item['author']} <br>\n" : '';
        $resultString .= ($item && array_key_exists('title', $item)) ? "{$item['title']} <br>\n" : '';
        $resultString .= (($holdings && array_key_exists('holdingsItem', $holdings) && array_key_exists('callNumber', $holdings['holdingsItem'])) ? "{$holdings['holdingsItem']['callNumber']} <br>\n" : '' );
        $resultString .= 
        	(($holdings && array_key_exists('policycode', $holdings)) ? "{$holdings['policycode']} " : '' )
        	. (($holdings && array_key_exists('holdingsItem', $holdings) && array_key_exists('location', $holdings['holdingsItem'])) ? "{$holdings['holdingsItem']['location']} <br>\n" : '' );
        return $resultString;
	}
	
	/**
	 * 
	 * Uses an ILS API service to lookup the barcode and get a control number (cat key)
	 * @param string $barcode
	 * @return array local_control_key, success (bool), and status ('success', 'not found', or other 'error')
	 */
	public function barcodeLookup($barcode){
		if (!$this->_barcodeUrl) {
			return array(
				'local_control_key' => '',
				'status' => 'error 000 - Not Configured',
				'resultFailureCode' => Rd_Sirsi_Result::FAILURE_NOTCONFIGURED,
				'success' => false
			);
		}
		$encodedBarcode = urlencode($barcode);
		$url = "{$this->_barcodeUrl}?barcode={$encodedBarcode}";
		// #TODO there should be a curl wrapper for this...
		$curl = curl_init($url);
		curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
		curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, true); //#TODO doesn't follow redirects to https
		$result = curl_exec($curl);
		$status = curl_getinfo($curl, CURLINFO_HTTP_CODE);
		if (0 == $status) {
			Rd_Debug::outData(array('failed_curl_sirsi_barcode' => curl_getinfo($curl)));
		}
		curl_close($curl);
		if (200 == $status) {
			return array(
				'local_control_key' => $result, 
				'status' => 'success',
				'success' => true
			);
		} else if (404 == $status) {
			return array(
				'local_control_key' => '',
				'status' => 'not found',
				'resultFailureCode' => Rd_Sirsi_Result::FAILURE_IDENTITY_NOT_FOUND,
				'success' => false
			);
		} else if (400 == $status || 401 == $status) { //#TODO currently the service spits out 401 when it means 400
			return array(
				'local_control_key' => '',
				'status' => 'invalid barcode',
				'resultFailureCode' => Rd_Sirsi_Result::FAILURE_IDENTITY_INVALID,
				'success' => false
			);
		} else if (401 == $status) { //#TODO currently unreachable due to above bug.
			return array(
				'local_control_key' => '',
				'status' => 'access declined',
				'resultFailureCode' => Rd_Sirsi_Result::FAILURE_CREDENTIAL_INVALID,
				'success' => false
			);
		} else {
			return array(
				'local_control_key' => '',
				'status' => "error {$status}",
				'resultFailureCode' => Rd_Sirsi_Result::FAILURE_UNCATEGORIZED,
				'success' => false
			);
		} 
	}

	/**
	 * (non-PHPdoc)
	 * @see Rd_Ils_Abstract::search()
	 * @param string $searchField
	 * @param string $searchTerm 
	 * @return Rd_Ils_Result
	 */
	public function search($searchField, $searchTerm)
	{
		if ('' == trim($searchTerm)) {
			return new Rd_Sirsi_Result(Rd_Sirsi_Result::FAILURE_IDENTITY_INVALID, 
				'No ' . ('barcode' == $searchField ? 'barcode' : 'title control number') . ' provided.'
			, null);			
		}
		if ($searchField == 'barcode')	{
			$barcodeResult = $this->barcodeLookup($searchTerm);
			Rd_Debug::out("ILS Barcode Lookup for {$searchTerm} result: {$barcodeResult['status']}");
			if (!$barcodeResult['success']) {
				return new Rd_Sirsi_Result(
					$barcodeResult['resultFailureCode'], 
					"{$searchTerm} : {$barcodeResult['status']}", 
					null
				);
			}
			Rd_Debug::out("Setting term to {$barcodeResult['local_control_key']} (Control Number)");
			$searchTerm = $barcodeResult['local_control_key'];
		}
		try {
			$this->xmlResults = trim($this->_doTitleQuery($searchTerm));
			return new Rd_Sirsi_Result(Rd_Sirsi_Result::SUCCESS, '', $this->xmlResults);
		} catch (Exception $e) {
			$this->xmlResults = null;
			return new Rd_Sirsi_Result(Rd_Sirsi_Result::FAILURE, $e->getMessage(), null);
		}
	}
	
	protected function _doTitleQuery($term)
	{
		$query = "@attr 1=12 \"{$term}\"";
		$startIndex = 0;
		$resultCount = 1;
		return $this->_zQuery($query, $startIndex, $resultCount);
	}
	
	public function supportsReserveRecords(){
		return true;
	}
	
	public function supportsItemLookup()
	{
		return true;
	}
	
	public function supportsItemSorting()
	{
		return true;
	}
	
	protected function _getShelvingKey($key, $keyType)
	{
		switch($keyType){
			case Rd_Ils::ITEM_CONTROL_KEY:
				$key = urlencode($key);
				$url = "{$this->_shelvingKeyUrl}?item_id={$key}";
				$curl = curl_init($url);
				curl_setopt($curl, CURLOPT_RETURNTRANSFER, TRUE);
				curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, TRUE); //#TODO doesn't follow redirects to https
				$result = json_decode(curl_exec($curl), TRUE);
				if(
					$result
					&& is_array($result)
					&& array_key_exists(0, $result)
					&& is_array($result[0])
					&& array_key_exists('shelving_key', $result[0])
				) {
					return $result[0]['shelving_key'];
				}
				return '';
			case Rd_Ils::TITLE_CONTROL_KEY: //#TODO not fully implemented
				if (!is_array($key)) {
					$key = array($key);
				}
				$url = "{$this->_shelvingKeyUrl}/" .implode(',', $key);
				$curl = curl_init($url);
				curl_setopt($curl, CURLOPT_RETURNTRANSFER, TRUE);
				curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, TRUE); //#TODO doesn't follow redirects to https
				$result = json_decode(curl_exec($curl), TRUE);
				if(
					$result
					&& is_array($result)
					&& array_key_exists(0, $result)
					&& is_array($result[0])
					&& array_key_exists($result[0], 'shelving_key')
				) {
					return $result[0]['shelving_key'];
				}
				return '';
		}
	}
	
	protected function _sortItems($keys, $keyType)
	{
		$return = array();
			switch($keyType){
			case Rd_Ils::ITEM_CONTROL_KEY://#TODO not implemented yet
				return $keys;
			case Rd_Ils::TITLE_CONTROL_KEY:
				if (!is_array($keys)) {
					$keys = array($keys);
				}
				//#TODO clean non-integer keys?
				$url = 'http://sirsi.lib.ncsu.edu/cgi-bin/item_shelving_key.pl';
				$post = 'catkey=' . implode('&catkey=', $keys);
				$curl = curl_init($url);
				curl_setopt($curl, CURLOPT_POSTFIELDS, $post);
				curl_setopt($curl, CURLOPT_POST, TRUE);
				curl_setopt($curl, CURLOPT_RETURNTRANSFER, TRUE);
				curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, TRUE); //#TODO doesn't follow redirects to https
				$rawResult = curl_exec($curl);
				$result = json_decode($rawResult, TRUE);
				if(
					$result
					&& is_array($result)
				) {
					foreach($result as $resultItem){
						if (
							is_array($resultItem) 
							&& array_key_exists('catkey', $resultItem)
							&& !in_array($resultItem['catkey'], $return)
						) {
							$return[] = $resultItem['catkey'];
						}
					}
					return $return;
				}
				Rd_Debug::out("Failed ILS sorting URL: {$url}");
				Rd_Debug::out('Failed Output: ' . htmlentities($rawResult));
				Rd_Layout::appendMessage('generalAlert', "Unable to sort on Call Number, ILS returned no data.");
				return $keys;
		}
	}
	
	protected function _zQuery($query, $start, $limit) //#TODO pull this out into a plug-in object
	{
		/*
		// Executes a z39.50 search
		// Get back our results in MARC format
		yaz_syntax($zConn, "xml");
		// We only want 10 records at a time -- "$start" is the record number we want to start from
		yaz_range($zConn, $start, 10);
		// Throw in some default attributes -- (4 (Structure) = 1 (Phrase), 3 (Position) = 3 (any position), 5 (Truncate) = 1 (Right Truncate)
		yaz_search($zConn,"rpn", $query);
		// yaz_wait actually executes the query
		yaz_wait();
		*/
		$xmlresults = '';
		$url = "{$this->zReflector}?host={$this->zHost}&port={$this->zPort}&db={$this->zDB}&query=" 
			. urlencode($query) 
			. "&start={$start}&limit={$limit}";
		$curl = curl_init($url);
		curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
		curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, true); //#TODO doesn't follow redirects to https
		$result = curl_exec($curl);
		$status = curl_getinfo($curl, CURLINFO_HTTP_CODE);
		if (0 == $status) {
			Rd_Debug::outData(array('failed_curl_sirsi_controlnumber' => curl_getinfo($curl)));
		}
		curl_close($curl);
		if (200 == $status) {
			return $result;
		} else {
			throw new Exception (
				"ILS said: status&gt; {$status}" 
				. (Rd_Debug::isEnabled() ? ' message&gt; ' . htmlentities($result) : '')
			);
		}
	}
	
	public function getCatalogUrlFor($key, $keyType)
	{
		return Rd_Registry::get('root:titleRecordUrl') . 'NCSU' . $key; //#TODO this is for title control, implement barcode as well
	}
	
	public static function filterIdentifier($identifier, $type = 'title')
	{
		switch($type) {
			case 'item':
				return $identifier;
			case 'title':
				if(strpos($identifier,'NCSU') === 0) {
					$identifier = substr($identifier,4);
				}
				return $identifier;
			default :
				return $identifier;
		}
	}
}

